package run.halo.app.service.impl;

import static run.halo.app.model.support.HaloConst.URL_SEPARATOR;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import run.halo.app.exception.NotFoundException;
import run.halo.app.model.dto.TagWithSiteCountDTO;
import run.halo.app.model.entity.Site;
import run.halo.app.model.entity.SiteTag;
import run.halo.app.model.entity.Tag;
import run.halo.app.model.enums.SiteStatus;
import run.halo.app.model.projection.TagSiteSiteCountProjection;
import run.halo.app.repository.SiteRepository;
import run.halo.app.repository.SiteTagRepository;
import run.halo.app.repository.TagRepository;
import run.halo.app.service.OptionService;
import run.halo.app.service.SiteTagService;
import run.halo.app.service.base.AbstractCrudService;
import run.halo.app.utils.ServiceUtils;

/**
 * Site tag service implementation.
 *
 * @author meazty
 * @since 2022/7/30 14:49
 **/
@Service
public class SiteTagServiceImpl extends AbstractCrudService<SiteTag, Integer>
    implements SiteTagService {

    private final SiteTagRepository siteTagRepository;

    private final SiteRepository siteRepository;

    private final TagRepository tagRepository;

    private final OptionService optionService;

    public SiteTagServiceImpl(SiteTagRepository siteTagRepository,
        SiteRepository siteRepository,
        TagRepository tagRepository,
        OptionService optionService) {
        super(siteTagRepository);
        this.siteTagRepository = siteTagRepository;
        this.siteRepository = siteRepository;
        this.tagRepository = tagRepository;
        this.optionService = optionService;
    }

    @Override
    public @NotNull List<Tag> listTagsBy(@NotNull Integer siteId) {
        Assert.notNull(siteId, "Site id must not be null");

        // Find all tag ids
        Set<Integer> tagIds = siteTagRepository.findAllTagIdsBySiteId(siteId);

        return tagRepository.findAllById(tagIds);
    }

    @Override
    public @NotNull List<TagWithSiteCountDTO> listTagWithCountDtos(@NotNull Sort sort) {
        Assert.notNull(sort, "Sort info must not be null");

        // Find all tags
        List<Tag> tags = tagRepository.findAll(sort);

        // Find all site count
        Map<Integer, Long> tagSiteCountMap = ServiceUtils
            .convertToMap(siteTagRepository.findSiteCount(), TagSiteSiteCountProjection::getTagId,
                TagSiteSiteCountProjection::getSiteCount);

        // Find site count
        return tags.stream().map(
            tag -> {
                TagWithSiteCountDTO tagWithCountOutputDTO =
                    new TagWithSiteCountDTO().convertFrom(tag);
                tagWithCountOutputDTO.setSiteCount(tagSiteCountMap.getOrDefault(tag.getId(), 0L));

                StringBuilder fullPath = new StringBuilder();

                if (optionService.isEnabledAbsolutePath()) {
                    fullPath.append(optionService.getBlogBaseUrl());
                }

                fullPath.append(URL_SEPARATOR)
                    .append(optionService.getTagsPrefix())
                    .append(URL_SEPARATOR)
                    .append(tag.getSlug())
                    .append(optionService.getPathSuffix());

                tagWithCountOutputDTO.setFullPath(fullPath.toString());

                return tagWithCountOutputDTO;
            }
        ).collect(Collectors.toList());
    }

    @Override
    public @NotNull Map<Integer, List<Tag>> listTagListMapBy(Collection<Integer> siteIds) {
        if (CollectionUtils.isEmpty(siteIds)) {
            return Collections.emptyMap();
        }

        // Find all site tags
        List<SiteTag> siteTags = siteTagRepository.findAllBySiteIdIn(siteIds);

        // Fetch tag ids
        Set<Integer> tagIds = ServiceUtils.fetchProperty(siteTags, SiteTag::getTagId);

        // Find all tags
        List<Tag> tags = tagRepository.findAllById(tagIds);

        // Convert to tag map
        Map<Integer, Tag> tagMap = ServiceUtils.convertToMap(tags, Tag::getId);

        // Create tag list map
        Map<Integer, List<Tag>> tagListMap = new HashMap<>(16);

        // Foreach and collect
        siteTags.forEach(
            siteTag -> tagListMap.computeIfAbsent(siteTag.getSiteId(), siteId -> new LinkedList<>())
                .add(tagMap.get(siteTag.getTagId())));

        return tagListMap;
    }


    @Override
    public @NotNull List<Site> listSitesBy(@NotNull Integer tagId) {
        Assert.notNull(tagId, "Tag id must not be null");

        // Find all site ids
        Set<Integer> siteIds = siteTagRepository.findAllSiteIdsByTagId(tagId);

        return siteRepository.findAllById(siteIds);
    }

    @Override
    public @NotNull List<Site> listSitesBy(@NotNull Integer tagId, @NotNull SiteStatus status) {
        Assert.notNull(tagId, "Tag id must not be null");
        Assert.notNull(status, "Site status must not be null");

        // Find all site ids
        Set<Integer> siteIds = siteTagRepository.findAllSiteIdsByTagId(tagId, status);

        return siteRepository.findAllById(siteIds);
    }

    @Override
    public @NotNull List<Site> listSitesBy(@NotNull String slug, @NotNull SiteStatus status) {
        Assert.notNull(slug, "Tag slug must not be null");
        Assert.notNull(status, "Site status must not be null");

        Tag tag = tagRepository.getBySlug(slug)
            .orElseThrow(() -> new NotFoundException("查询不到该标签的信息").setErrorData(slug));

        Set<Integer> siteIds = siteTagRepository.findAllSiteIdsByTagId(tag.getId(), status);

        return siteRepository.findAllById(siteIds);
    }

    @Override
    public Page<Site> pageSitesBy(@NotNull Integer tagId, Pageable pageable) {
        Assert.notNull(tagId, "Tag id must not be null");
        Assert.notNull(pageable, "Page info must not be null");

        // Find all site ids
        Set<Integer> siteIds = siteTagRepository.findAllSiteIdsByTagId(tagId);

        return siteRepository.findAllByIdIn(siteIds, pageable);
    }

    @Override
    public Page<Site> pageSitesBy(@NotNull Integer tagId, @NotNull SiteStatus status, Pageable pageable) {
        Assert.notNull(tagId, "Tag id must not be null");
        Assert.notNull(status, "Site status must not be null");
        Assert.notNull(pageable, "Page info must not be null");

        // Find all site ids
        Set<Integer> siteIds = siteTagRepository.findAllSiteIdsByTagId(tagId, status);

        return siteRepository.findAllByIdIn(siteIds, pageable);
    }

    @Override
    public @NotNull List<SiteTag> mergeOrCreateByIfAbsent(@NotNull Integer siteId, Set<Integer> tagIds) {
        Assert.notNull(siteId, "Site id must not be null");

        if (CollectionUtils.isEmpty(tagIds)) {
            return Collections.emptyList();
        }

        // Create site tags
        List<SiteTag> siteTagsStaging = tagIds.stream().map(tagId -> {
            // Build site tag
            SiteTag siteTag = new SiteTag();
            siteTag.setSiteId(siteId);
            siteTag.setTagId(tagId);
            return siteTag;
        }).collect(Collectors.toList());

        List<SiteTag> siteTagsToRemove = new LinkedList<>();
        List<SiteTag> siteTagsToCreate = new LinkedList<>();

        List<SiteTag> siteTags = siteTagRepository.findAllBySiteId(siteId);

        siteTags.forEach(siteTag -> {
            if (!siteTagsStaging.contains(siteTag)) {
                siteTagsToRemove.add(siteTag);
            }
        });

        siteTagsStaging.forEach(siteTagStaging -> {
            if (!siteTags.contains(siteTagStaging)) {
                siteTagsToCreate.add(siteTagStaging);
            }
        });

        // Remove site tags
        removeAll(siteTagsToRemove);

        // Remove all site tags need to remove
        siteTags.removeAll(siteTagsToRemove);

        // Add all created site tags
        siteTags.addAll(createInBatch(siteTagsToCreate));

        // Return site tags
        return siteTags;
    }

    @Override
    public @NotNull List<SiteTag> listBySiteId(@NotNull Integer siteId) {
        Assert.notNull(siteId, "Site id must not be null");

        return siteTagRepository.findAllBySiteId(siteId);
    }

    @Override
    public @NotNull List<SiteTag> listByTagId(@NotNull Integer tagId) {
        Assert.notNull(tagId, "Tag id must not be null");

        return siteTagRepository.findAllByTagId(tagId);
    }

    @Override
    public @NotNull Set<Integer> listTagIdsBySiteId(@NotNull Integer siteId) {
        Assert.notNull(siteId, "Site id must not be null");

        return siteTagRepository.findAllTagIdsBySiteId(siteId);
    }

    @Override
    public @NotNull List<SiteTag> removeBySiteId(@NotNull Integer siteId) {
        Assert.notNull(siteId, "Site id must not be null");

        return siteTagRepository.deleteBySiteId(siteId);
    }

    @Override
    public @NotNull List<SiteTag> removeByTagId(@NotNull Integer tagId) {
        Assert.notNull(tagId, "Tag id must not be null");

        return siteTagRepository.deleteByTagId(tagId);
    }
}
